#include "common.h"
#include "trigger.h"
#include "init.h"
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/kdev_t.h>
#include <linux/atomic.h>
#include <linux/slab.h>
#include <linux/version.h>


#define TRIGGER_CLASS_NAME "trigger"

static struct class *_trigger_class = NULL;

static size_t trigger_class_refcount = 0;

static DEFINE_SPINLOCK(trigger_class_lock);



// get trigger device class
struct class *get_trigger_class(void) {
	struct class *ret = NULL;
	spin_lock(&trigger_class_lock);
	if (_trigger_class == NULL) {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
		ret = class_create(TRIGGER_CLASS_NAME);
#else
		ret = class_create(THIS_MODULE, TRIGGER_CLASS_NAME);
#endif
		if (IS_ERR(ret)) {
			spin_unlock(&trigger_class_lock);
			return ret;
		}
		_trigger_class = ret;
	} else {
		ret = _trigger_class;
	}
	trigger_class_refcount++;
	spin_unlock(&trigger_class_lock);
	return ret;
}

// release trigger device class
void put_trigger_class(void) {
	int ref = 0;
	spin_lock(&trigger_class_lock);
	ref = --trigger_class_refcount;
	if (ref == 0) {
		PR_INFO("trigger class not referenced anymore, release trigger class");
		class_destroy(_trigger_class);
	}
	spin_unlock(&trigger_class_lock);
}

struct trigger_init_cfg {
	struct init_block init;
	const char *device_name;

	// trigger device number
	dev_t dev_id;

	// char device struct
	struct cdev cdev;

	struct file_operations fops;

	// device file
	struct device *device;

	// trigger on fn
	trigger_on_fn_t on_fn;

	// trigger off fn
	trigger_off_fn_t off_fn;

	// payload
	void *data;

	atomic_t pressed;
};

static int trigger_open(struct inode *inode, struct file *file)
{
	int ret;
	const struct file_operations *fops;
	struct trigger_init_cfg *cfg;

	if (!try_module_get(THIS_MODULE)) {
		return -EINVAL;
	};

	fops = file->f_op;
	cfg = container_of(fops, struct trigger_init_cfg, fops);
	
	ret = atomic_xchg_acquire(&cfg->pressed, 1);
	if (ret != 0) {
		module_put(THIS_MODULE);
		return -EBUSY;
	}
	cfg->on_fn(cfg->data);
	return 0;
}

static int trigger_release(struct inode* inode, struct file *file)
{
	int ret;
	const struct file_operations *fops;
	struct trigger_init_cfg *cfg;

	fops = file->f_op;
	cfg = container_of(fops, struct trigger_init_cfg, fops);

	ret = atomic_xchg_release(&cfg->pressed, 0);
	if (ret != 1) {
		return -EINVAL;
	}

	cfg->off_fn(cfg->data);
	module_put(THIS_MODULE);
	return 0;
}

static ssize_t trigger_read(struct file *file, char * __user buf, size_t size, loff_t *loff)
{
	return 0;
}

static ssize_t trigger_write(struct file *file, const char * __user buf, size_t size, loff_t *loff)
{
	return -EINVAL;
}

// default trigger fops
static struct file_operations default_fops = {
	.open = trigger_open,
	.release = trigger_release,
	.read = trigger_read,
	.write = trigger_write,
};

// trigger device init function
static int init_trigger(struct init_block *init) {
	int ret = 0;
	struct trigger_init_cfg *cfg = container_of(init, struct trigger_init_cfg, init);
	struct class *trigger_class = get_trigger_class();
	if (IS_ERR(trigger_class)) {
		PR_ERR("try init trigger class failed");
		return PTR_ERR(trigger_class);
	}

	ret = alloc_chrdev_region(&cfg->dev_id, 10, 1, cfg->device_name);
	if (ret) {
		PR_ERR("alloc char dev region failed");
		put_trigger_class();
		return ret;
	}

	cdev_init(&cfg->cdev, &cfg->fops);
	cfg->cdev.owner = THIS_MODULE;

	PR_INFO("add trigger char device, name: %s", cfg->device_name);
	ret = cdev_add(&cfg->cdev, cfg->dev_id, 1);
	if (ret) {
		PR_ERR("add trigger char device failed, errno: %d", ret);
		cfg->dev_id = 0;
		unregister_chrdev_region(cfg->dev_id, 1);
		put_trigger_class();
		return ret;
	}

	PR_INFO("create trigger device");
	cfg->device = device_create(trigger_class, NULL, cfg->dev_id, NULL, cfg->device_name);
	if (cfg->device == NULL) {
		PR_ERR("create trigger device failed");
		put_trigger_class();
		return -EBUSY;
	}
	return ret;
}

// trigger device cleanup function
static void cleanup_trigger(struct init_block *init) {
	struct trigger_init_cfg *cfg = container_of(init, struct trigger_init_cfg, init);

	if (cfg->device != NULL) {
		struct class *trigger_class = get_trigger_class();
		PR_INFO("cleanup trigger device");
		device_destroy(trigger_class, cfg->dev_id);
		PR_INFO("finish cleanup trigger device");
		put_trigger_class();
		put_trigger_class();
	}

	if (cfg->dev_id != 0) {
		cdev_del(&cfg->cdev);
		unregister_chrdev_region(cfg->dev_id, 1);
	}

	return;
}

// trigger device drop function
static void drop_trigger(struct init_block *init) {
	struct trigger_init_cfg *cfg = container_of(init, struct trigger_init_cfg, init);
	kfree(cfg);
	return;
}

// build a new trigger device with device name and on off trigger function
int new_trigger(const char *device_name, trigger_on_fn_t on_fn, trigger_off_fn_t off_fn, void *data) {
	struct trigger_init_cfg *cfg = kmalloc(sizeof(struct trigger_init_cfg), GFP_KERNEL); 

	if (cfg == NULL) {
		return -ENOMEM;
	}

	strncpy(cfg->init.name, device_name, BLOCK_NAME_LEN);
	cfg->init.init_fn = init_trigger;
	cfg->init.drop_fn = drop_trigger;
	cfg->init.cleanup_fn = cleanup_trigger;
	cfg->device_name = device_name;
	cfg->dev_id = 0;

	cfg->fops = default_fops;
	cfg->device = NULL;

	cfg->on_fn = on_fn;
	cfg->off_fn = off_fn;
	cfg->data = data;
	atomic_set(&cfg->pressed, 0);

	return try_init(&cfg->init);
}


